// 2024/1/15: 首个版本
// 2024/8/16: 增加ConnectTimeout
// 2024/8/16: 修正被动断开后无法连接问题
// 2024/8/16: 使用更完善的连接超时机制
// 2024/8/19: 降低传输延迟
// 2024/8/20: 接收端检查心跳。新增IsAcceptFailed
// 2024/8/22: 捕获设置SendTimeout异常
// 2024/8/22: 修正接收数据漏帧问题
// 2024/8/26: 增加ConnectTimeout至600ms
// 2024/8/29: 剥离负责实际传输的代码
// 2025/7/3: 新增SendPacket.GetActualPacketSize
// 2025/7/22: 新增ReceiveStream.VersionMismatch

using System;
using System.Collections.Generic;
using ASEva;

namespace RemoteConsole
{
    class PacketMagic
    {
        public static byte[] Head = [ 55, 13, 34, 8, 233, 144, 21, 89 ];
        public static ulong Tail = 0xfe01dc23ba459867;
    }

    class ReceiveBinary
    {
        public uint Index { get; set; } // 1~
        public byte[] Data { get; set; }
    }

    class ReceiveStream
    {
        public void Append(byte[] received)
        {
            bufferList.Add(received);
        }

        public ReceiveBinary[] Parse()
        {
            List<byte[]> packetList = new List<byte[]>();
            List<byte> curPacket = null;
            int magicIndex = 0;
            foreach (var buffer in bufferList)
            {
                for (int i = 0; i < buffer.Length; i++)
                {
                    if (curPacket != null) curPacket.Add(buffer[i]);
                    if (buffer[i] != PacketMagic.Head[magicIndex])
                    {
                        if (magicIndex == 6 && buffer[i] >= 21 && buffer[i] < 30 && !VersionMismatch)
                        {
                            AgencyLocal.Log(AgencyLocal.GetAppLanguage() == Language.Chinese ? "通讯协议版本不匹配。" : "Communication protocol version mismatch.", LogLevel.Warning);
                            VersionMismatch = true;
                        }
                        magicIndex = 0;
                    }
                    if (buffer[i] == PacketMagic.Head[magicIndex])
                        {
                            if (++magicIndex == 8)
                            {
                                magicIndex = 0;
                                if (curPacket != null)
                                {
                                    curPacket.RemoveRange(curPacket.Count - 8, 8);
                                    packetList.Add(curPacket.ToArray());
                                }
                                curPacket = new List<byte>();
                                curPacket.AddRange(PacketMagic.Head);
                            }
                        }
                }
            }
            if (curPacket != null) packetList.Add(curPacket.ToArray());

            bufferList.Clear();

            var packets = packetList.ToArray();
            var outputs = new List<ReceiveBinary>();
            for (int n = 0; n < packets.Length; n++)
            {
                bool notComplete = false;
                var output = parsePacket(packets[n], out notComplete);
                if (output != null)
                {
                    if (output.Index > 0) outputs.Add(output); // remove heart beat
                }
                else
                {
                    if (n == packets.Length - 1 && notComplete) bufferList.Add(packets[n]);
                }
            }
            return outputs.ToArray();
        }

        public bool VersionMismatch { get; private set; } = false;

        private ReceiveBinary parsePacket(byte[] packet, out bool notComplete)
        {
            if (packet.Length < 24)
            {
                notComplete = true;
                return null;
            }

            int binarySize = BitConverter.ToInt32(packet.AsSpan(8, 4));
            var fullSize = SendPacket.GetActualPacketSize(binarySize);
            if (packet.Length < fullSize)
            {
                notComplete = true;
                return null;
            }

            notComplete = false;
            if (packet.Length != fullSize) return null;

            var ulongCount = packet.Length / 8;
            ulong code = 0;
            for (int i = 0; i < ulongCount; i++)
            {
                code ^= BitConverter.ToUInt64(packet.AsSpan(i * 8, 8));
            }
            if (code != PacketMagic.Tail) return null;

            uint index = BitConverter.ToUInt32(packet.AsSpan(12, 4));
            var data = new byte[binarySize];
            if (binarySize > 0) Array.Copy(packet, 16, data, 0, binarySize);
            return new ReceiveBinary
            {
                Index = index,
                Data = data,
            };
        }

        private List<byte[]> bufferList = new List<byte[]>();
    }

    class SendPacket
    {
        public static int GetActualPacketSize(int dataSize)
        {
            var alignBytes = dataSize % 8 == 0 ? 0 : (8 - dataSize % 8);
            return 16 + dataSize + alignBytes + 8;
        }

        public byte[] Generate(byte[] data)
        {
            var fullBinary = new byte[GetActualPacketSize(data.Length)];
            Array.Copy(PacketMagic.Head, 0, fullBinary, 0, 8);
            BitConverter.TryWriteBytes(fullBinary.AsSpan(8, 4), data.Length);
            BitConverter.TryWriteBytes(fullBinary.AsSpan(12, 4), ++sendFrameCounter);
            Array.Copy(data, 0, fullBinary, 16, data.Length);
            assignValidationCode(fullBinary);
            return fullBinary;
        }

        public byte[] GenerateHeartBeat()
        {
            var heartBeatBinary = new byte[24];
            Array.Copy(PacketMagic.Head, 0, heartBeatBinary, 0, 8);
            assignValidationCode(heartBeatBinary);
            return heartBeatBinary;
        }

        public void Reset()
        {
            sendFrameCounter = 0;
        }

        private void assignValidationCode(byte[] binary)
        {
            var ulongCount = binary.Length / 8;
            ulong code = 0;
            for (int i = 0; i < ulongCount; i++)
            {
                code ^= BitConverter.ToUInt64(binary.AsSpan(i * 8, 8));
            }
            code ^= PacketMagic.Tail;
            BitConverter.TryWriteBytes(binary.AsSpan((ulongCount - 1) * 8, 8), code);
        }

        private uint sendFrameCounter = 0;
    }
}